﻿using Jint.Native;
using Jint.Runtime;
using Jint.Runtime.Interop;

namespace Jint.Tests.Runtime;

public class NullPropagation
{
    public class NullPropagationReferenceResolver : IReferenceResolver
    {
        public bool TryUnresolvableReference(Engine engine, Reference reference, out JsValue value)
        {
            value = reference.Base;
            return true;
        }

        public bool TryPropertyReference(Engine engine, Reference reference, ref JsValue value)
        {
            return value.IsNull() || value.IsUndefined();
        }

        public bool TryGetCallable(Engine engine, object callee, out JsValue value)
        {
            if (callee is Reference reference)
            {
                var name = reference.ReferencedName.AsString();
                if (name == "filter")
                {
                    value = new ClrFunction(engine, "map", (thisObj, values) => engine.Realm.Intrinsics.Array.ArrayCreate(0));
                    return true;
                }
            }

            value = new ClrFunction(engine, "anonymous", (thisObj, values) => thisObj);
            return true;
        }

        public bool CheckCoercible(JsValue value)
        {
            return true;
        }
    }

    [Fact]
    public void CanCallFilterOnNull()
    {
        var engine = new Engine(cfg => cfg.SetReferencesResolver(new NullPropagationReferenceResolver()));

        const string Script = @"
var input = {};

var output = { Tags : input.Tags.filter(x=>x!=null) };
";

        engine.Execute(Script);

        var output = engine.GetValue("output").AsObject();

        Assert.True(output.Get("Tags").IsArray());
    }

    [Fact]
    public void NullPropagationTest()
    {
        var engine = new Engine(cfg => cfg.SetReferencesResolver(new NullPropagationReferenceResolver()));

        const string Script = @"
var input = {
	Address : null
};

var address = input.Address;
var city = input.Address.City;
var length = input.Address.City.length;

var output = {
	Count1 : input.Address.City.length,
	Count2 : this.XYZ.length
};
";

        engine.Execute(Script);

        var address = engine.GetValue("address");
        var city = engine.GetValue("city");
        var length = engine.GetValue("length");
        var output = engine.GetValue("output").AsObject();

        Assert.Equal(JsValue.Null, address);
        Assert.Equal(JsValue.Null, city);
        Assert.Equal(JsValue.Null, length);

        Assert.Equal(JsValue.Null, output.Get("Count1"));
        Assert.Equal(JsValue.Undefined, output.Get("Count2"));
    }

    [Fact]
    public void NullPropagationFromArg()
    {
        var engine = new Engine(cfg => cfg.SetReferencesResolver(new NullPropagationReferenceResolver()));


        const string Script = @"
function test(arg) {
    return arg.Name;
}

function test2(arg) {
    return arg.Name.toUpperCase();
}
";
        engine.Execute(Script);
        var result = engine.Invoke("test", JsValue.Null);

        Assert.Equal(JsValue.Null, result);

        result = engine.Invoke("test2", JsValue.Null);

        Assert.Equal(JsValue.Null, result);
    }

    [Fact]
    public void NullPropagationShouldNotAffectOperators()
    {
        var engine = new Engine(cfg => cfg.SetReferencesResolver(new NullPropagationReferenceResolver()));

        var jsObject = engine.Realm.Intrinsics.Object.Construct(Arguments.Empty);
        jsObject.Set("NullField", JsValue.Null);

        var script = @"
this.is_nullfield_not_null = this.NullField !== null;
this.is_notnullfield_not_null = this.NotNullField !== null;
this.has_emptyfield_not_null = this.EmptyField !== null;
";

        var wrapperScript = $@"function ExecutePatchScript(docInner){{ (function(doc){{ {script} }}).apply(docInner); }};";

        engine.Execute(wrapperScript, "main.js");

        engine.Invoke("ExecutePatchScript", jsObject);

        Assert.False(jsObject.Get("is_nullfield_not_null").AsBoolean());
        Assert.True(jsObject.Get("is_notnullfield_not_null").AsBoolean());
        Assert.True(jsObject.Get("has_emptyfield_not_null").AsBoolean());
    }
}